package simulator.eventHandler;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Random;

import simulator.Configuration;
import simulator.Event;
import simulator.EventHandler;
import simulator.Metadata;
import simulator.Result;
import simulator.Unit;
import simulator.unit.*;

public class RandomDistributeEventHandler2 implements EventHandler {

	private final int replicaCount = 3;
	private byte[] availableCount = null;
	private byte[] durableCount = null;
	private byte[] latentDefectCount = null;

	private int unavailableSliceCount = 0;
	private int unDurableSliceCount = 0;

	public Result end() {
		Result ret = new Result();
		ret.unAvailableCount = this.unavailableSliceCount;
		ret.unDurableCount = this.unDurableSliceCount;
		return ret;
	}


	public void handleEvent(Event e) {
		if (e.getType() == Event.EventType.Failure) {
			handleFailure(e.getUnit(), e.getTime(), e);
		} else if (e.getType() == Event.EventType.Recovered) {
			handleRecovery(e.getUnit(), e.getTime(), e);
		} else if (e.getType() == Event.EventType.LatentDefect) {
			handleLatentDefect(e.getUnit(), e.getTime(), e);
		} else if (e.getType() == Event.EventType.ScrubStart) {
			handleScrubStart(e.getUnit(), e.getTime(), e);
		} else if (e.getType() == Event.EventType.ScrubComplete) {
			handleScrubComplete(e.getUnit(), e.getTime(), e);
		} else {
			throw new RuntimeException("Unknown event " + e.getType());
		}
	}

	private void handleFailure(Unit u, double time, Event e) {
		if (u instanceof Machine) {
			for (Unit child : u.getChildren()) {
				if (child.getMetadata().sliceCount == 0)
					continue;
				for (int i=0; i<child.getMetadata().sliceCount; i++) {
					int sliceIndex = child.getMetadata().slices[i];
					availableCount[sliceIndex]--;
					assert (availableCount[sliceIndex] >= 0);
					if (availableCount[sliceIndex] == 0) {
						// System.out.println("slice "+sliceIndex+" becomes unavailable at time "+time);
						unavailableSliceCount++;
					}
				}
			}
		} else if (u instanceof Disk) {
			for (int i=0; i<u.getMetadata().sliceCount; i++) {
				int sliceIndex = u.getMetadata().slices[i];
				durableCount[sliceIndex]--;

				// This is to turn a latent defect into a failure. It should
				// not be counted twice.
				if (u.getMetadata().defectedSlices!=null&&u.getMetadata().defectedSlices.contains(sliceIndex)) {
					//if(sliceIndex==3034403)
					//	System.out.println("3034403 replaced by "+u.getID());
					latentDefectCount[sliceIndex]--;
					if (latentDefectCount[sliceIndex] < 0)
						throw new RuntimeException("weird");
				}

				if (durableCount[sliceIndex] < 0)
					throw new RuntimeException("weird");

				if (durableCount[sliceIndex] == 0) {
					// System.out.println("slice "+sliceIndex+" becomes undurable at time "+time);
					unDurableSliceCount++;
				}
				if (durableCount[sliceIndex] == 1
						&& latentDefectCount[sliceIndex] > 0) {
					// System.out.println("slice "+sliceIndex+" becomes undurable at time "+time);
					unDurableSliceCount++;
				}

			}
			u.getMetadata().defectedSlices=null;
			u.getMetadata().tmp=null;
		} else {
			for (Unit tmp : u.getChildren())
				handleFailure(tmp, time, e);
		}
	}

	private void handleRecovery(Unit u, double time, Event e) {
		if (u instanceof Machine) {
			for (Unit child : u.getChildren()) {
				for (int i=0; i<child.getMetadata().sliceCount; i++) {
					int sliceIndex = child.getMetadata().slices[i];
					// if(availableCount[sliceIndex]==0)
					// System.out.println("slice "+sliceIndex+" recovered at "+time);
					availableCount[sliceIndex]++;
					assert (availableCount[sliceIndex] <= replicaCount);
				}
			}
		} else if (u instanceof Disk) {
			for (int i=0; i<u.getMetadata().sliceCount; i++) {
				int sliceIndex = u.getMetadata().slices[i];
				durableCount[sliceIndex]++;
			}
			u.getMetadata().defectedSlices = null;
			u.getMetadata().tmp = null;
		} else {
			for (Unit tmp : u.getChildren())
				handleRecovery(tmp, time, e);
		}
	}

	private void handleLatentDefect(Unit u, double time, Event e) {
		if (u instanceof Disk) {
			if (u.getMetadata().sliceCount == 0)
				return;
			int index = r.nextInt(u.getMetadata().sliceCount);
			int sliceIndex = u.getMetadata().slices[index];
			if(u.getMetadata().defectedSlices!=null&&u.getMetadata().defectedSlices.contains(sliceIndex))
				return;
			//if(sliceIndex==3034403)
			//	System.out.println("3034403 is defected at "+u.getID());
			latentDefectCount[sliceIndex]++;
			if (u.getMetadata().defectedSlices == null)
				u.getMetadata().defectedSlices = new HashSet<Integer>();
			u.getMetadata().defectedSlices.add(sliceIndex);

			if (durableCount[sliceIndex] == 1) {
				// System.out.println("slice "+sliceIndex+" becomes undurable at time "+time);
				unDurableSliceCount++;
			}
		} else
			throw new RuntimeException(
					"Latent defect should only happen for disk");
	}

	private void handleScrubStart(Unit u, double time, Event e) {
		if (u instanceof Disk) {
			if (u.getMetadata().defectedSlices == null)
				return;
			u.getMetadata().tmp = (HashSet) u.getMetadata().defectedSlices
					.clone();
		} else
			throw new RuntimeException(
					"Scrub start defect should only happen for disk");
	}

	private void handleScrubComplete(Unit u, double time, Event e) {
		if (u instanceof Disk) {
			if (u.getMetadata().tmp == null)
				return;
			for (Integer sliceIndex : u.getMetadata().tmp) {
				//if(sliceIndex==3034403)
				//	System.out.println("3034403 is fixed at "+u.getID());
				latentDefectCount[sliceIndex]--;
				if (latentDefectCount[sliceIndex] < 0)
					throw new RuntimeException("weird "+latentDefectCount[sliceIndex]+" "+sliceIndex);
				u.getMetadata().defectedSlices.remove(sliceIndex);
			}
			u.getMetadata().tmp = null;
		} else
			throw new RuntimeException(
					"Scrub complete should only happen for disk");
	}

	public void start(Unit root) {
		distributeSlices(root);
	}

	private void distributeSlices(Unit root) {
		int diskCount = getDiskCount(root);
		int totalSlices = diskCount * Configuration.slicesPerDisk
				/ replicaCount * 5 / 6;

		System.out.println("totalSlices=" + totalSlices+" diskCount="+diskCount);
		availableCount = new byte[totalSlices];
		durableCount = new byte[totalSlices];
		latentDefectCount = new byte[totalSlices];
		ArrayList<Unit> disks = new ArrayList<Unit>(diskCount);
		getAllDisks(root, disks);
		LinkedList<Unit> tmp = new LinkedList<Unit>();
		for (int i = 0; i < totalSlices; i++) {
			tmp.clear();
			for (int j = 0; j < replicaCount; j++) {
				distributeOneSliceToOneDisk(i, tmp, disks);
			}
			assert (tmp.size() == replicaCount);
			availableCount[i] = replicaCount;
			durableCount[i] = replicaCount;
		}
	}

	private Random r = new Random();

	private void distributeOneSliceToOneDisk(int sliceIndex,
			LinkedList<Unit> previous, ArrayList<Unit> disks) {
		int retryCount = 0;
		while (true) {
			retryCount++;
			boolean success = true;
			int diskIndex = r.nextInt(disks.size());
			Unit disk = disks.get(diskIndex);
			if (disk.getMetadata().sliceCount >= Configuration.slicesPerDisk) {
				success = false;
				continue;
			}
			for (Unit u : previous) {
				// Two slices cannot be on the same rack
				if (!(u.getParent().getParent() instanceof Rack))
					throw new RuntimeException(
							"Cannot find rack. Maybe wrong configuration");
				if (u.getParent().getParent() == disk.getParent().getParent()) {
					success = false;
					break;
				}
			}
			if (success) {
				//if(sliceIndex==3034403)
				//	System.out.println("assign 3034403 to "+disk.getID());
				Metadata m = disk.getMetadata();
				if (m.slices == null)
					m.slices = new int[Configuration.slicesPerDisk];
				m.slices[m.sliceCount] = sliceIndex;
				m.sliceCount++;
				previous.add(disk);
				return;
			}
			if (retryCount > 100) {
				System.out.println("Unable to distribute slice " + sliceIndex);
				return;
			}

		}
	}

	private int getDiskCount(Unit u) {
		int ret = 0;
		for (Unit tmp : u.getChildren()) {
			if (tmp instanceof Disk)
				ret++;
			else
				ret += getDiskCount(tmp);
		}
		return ret;
	}

	private void getAllDisks(Unit u, ArrayList<Unit> disks) {
		for (Unit tmp : u.getChildren()) {
			if (tmp instanceof Disk)
				disks.add(tmp);
			else
				getAllDisks(tmp, disks);
		}
	}

}
